/*
 * Copyright 2007. Mount Sinai Hospital, Toronto, Canada.
 * 
 * Licensed under the GNU Lesser General Public License, Version 2. You
 * can find a copy of the license at:
 * 
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
 * 
 * IN NO EVENT SHALL MOUNT SINAI HOSPITAL BE LIABLE TO ANY PARTY FOR DIRECT, 
 * INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST 
 * PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, 
 * EVEN IF MOUNT SINAI HOSPITAL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * 
 * MOUNT SINAI HOSPITAL SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE AND 
 * ACCOMPANYING DOCUMENTATION, IF ANY, PROVIDED HEREUNDER IS PROVIDED "AS IS". 
 * MOUNT SINAI HOSPITAL HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, 
 * UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 
 */
package com.sinai.mshab.server.sql;

import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

import com.sinai.mshab.client.MSHABConfig;

/**
 * The DbFilterGroup class stores groups of {@link DbField}s allowing for
 * proper SQL grouping in WHERE clauses and allowing for custom SQL operators
 * between the contained DbFilters.
 * 
 * @author Arthur Kalmenson
 */
public class DbFilterGroup {

	/**
	 * The SQL operator between elements in the group.
	 */
	private String operator = null;

	/**
	 * The group of filters.
	 */
	private Set group = null;

	/**
	 * Creates a new DbFilterGroup using <code>operator</code> as the SQL
	 * operator between the containing elements. If <code>operator</code> is
	 * not a valid SQL operator an {@link IllegalArgumentException} will be
	 * thrown.
	 * 
	 * @param operator
	 *            the SQL operator between the grouped elements.
	 */
	public DbFilterGroup(String operator) {

		if (!validOperator(operator)) {
			throw new IllegalArgumentException(
					MSHABConfig.MSHAB_ERROR_INVALID_SQL_OPERATOR + operator);
		}

		this.operator = operator.toUpperCase();
		group = new LinkedHashSet();
	}

	/**
	 * Adds filter to the group of DbFilters. Note that filters with the same
	 * information will not appear twice.
	 * 
	 * @param filter
	 *            the {@link DbFilter} to add to the group of other DbFilters.
	 */
	public void add(DbFilter filter) {
		group.add(filter);
	}

	/**
	 * Adds a DbFilterGroup to this group.
	 * 
	 * @param filterGroup
	 *            the group to add.
	 */
	public void add(DbFilterGroup filterGroup) {
		group.add(filterGroup);
	}

	/**
	 * Returns the group of elements in this DbFilterGroup in a format that can
	 * be used in SQL, i.e. the group is contained in parenthesis and each
	 * element is separated by <code>operator</code>. Note that the values of
	 * each key must still be filled in a PreparedStatement. Furthermore, if
	 * DbFilterGroup contain any groups, an {@link IllegalStateException} will
	 * be thrown.
	 * 
	 * @return the group of elements in this DbFilterGroup in a format that can
	 *         be used in SQL.
	 */
	public String getSQL() {

		// if the group is empty, throw an exception.
		if (group.isEmpty()) {
			throw new IllegalStateException(MSHABConfig.MSHAB_ERROR_EMPTY_GROUP);
		}

		String result = "(";

		// if we have multiple entries, then we need to add the operator between
		// them.
		boolean multiple = false;

		// go through everything in group and create the resulting SQL ready
		// String.
		for (Iterator filters = group.iterator(); filters.hasNext();) {
			Object filterOrGroup = filters.next();

			// if we've had multiple entries, then we need to separate them
			// with the operator.
			if (multiple) {
				result += " " + operator + " ";
			}

			// if filterOrGroup is a DbFilterGroup, then we recursively grab
			// it's contents.
			if (filterOrGroup instanceof DbFilterGroup) {
				DbFilterGroup filterGroup = (DbFilterGroup) filterOrGroup;
				result += filterGroup.getSQL();

				// since we've already added something, that means multiple is
				// true.
				multiple = true;
			} else if (filterOrGroup instanceof DbFilter) {

				// grab the DbFilter and add it's contents to the result.
				DbFilter filter = (DbFilter) filterOrGroup;
				result += filter.getTable() + "." + filter.getField() + " "
						+ filter.getOperator() + " ?";

				// since we've already added something, that means multiple is
				// true.
				multiple = true;
			}
		}

		// now go through the rest of the elements separating them with the
		// given operator.

		result += ")";

		return result;
	}

	/**
	 * Returns the operator between the
	 * 
	 * @return
	 */
	public String getOperator() {
		return operator;
	}

	/**
	 * Returns a flat Set containing all the {@link DbFilter}s in this group.
	 * This is done by extracting the DbFilters from this DbFilterGroup and any
	 * internal DbFilterGroups that it might contain. Note that the DbFilters
	 * will appear in the order in which they were added.
	 * 
	 * @return a flat Set containing all the {@link DbFilter}s in this group.
	 */
	public Set getFlatSet() {
		Set flatSet = new LinkedHashSet();

		for (Iterator elemnt = group.iterator(); elemnt.hasNext();) {
			Object filterOrGroup = (Object) elemnt.next();

			// if we're dealing with a DbFilter, we just add it. However, if
			// we're dealing with a group, we recursively call this function and
			// add that set.
			if (filterOrGroup instanceof DbFilter) {
				flatSet.add(filterOrGroup);
			} else if (filterOrGroup instanceof DbFilterGroup) {
				DbFilterGroup group = (DbFilterGroup) filterOrGroup;
				flatSet.addAll(group.getFlatSet());
			}
		}

		return flatSet;
	}

	/**
	 * Returns true if this group is empty and false otherwise.
	 * 
	 * @return true if this group is empty and false otherwise.
	 */
	public boolean isEmpty() {
		return group.isEmpty();
	}

	/**
	 * Returns true if <code>operator</code> is a valid SQL operator and false
	 * otherwise. Valid operators are "AND", "OR", and "NOT".
	 * 
	 * @param operator
	 *            the SQL operator to check.
	 * @return true if <code>operator</code> is a valid SQL operator and false
	 *         otherwise.
	 */
	private boolean validOperator(String operator) {
		boolean result = false;

		// as long as we don't have a null operator.
		if (operator != null) {
			// make the operator lower case since SQL is case insensitive.
			String lowerCase = operator.toUpperCase();

			// check to insure it's a valid operator.
			if (lowerCase.equals("AND") || lowerCase.equals("OR")) {
				result = true;
			}
		}

		return result;
	}
}
